#include "../XMLTools.h"
#include "../FactoryPluginLoader.h"
#include "SensorFactory.h"
#include "../Model/ModelProcessorFactory.h"
#include "../Model/ModelReaderXML.h"
#include "../Exceptions.h"

#include <map>
#include <vector>
#include <set>
#include <filesystem>

#include "MotionReaderXML.h"

using namespace MMM;

const std::string MotionReaderXML::ERROR_MESSAGE = "Could not load motion file! ";

MotionReaderXML::MotionReaderXML(const std::vector<std::string> &additionalLibPaths, bool ignoreStandardLibPaths) {
    std::vector<std::string> libPaths;
    if (!ignoreStandardLibPaths) libPaths.push_back(getStandardLibPath());
    for (const auto &additionalLibPath : additionalLibPaths) libPaths.push_back(additionalLibPath);
    boost::shared_ptr<FactoryPluginLoader<SensorFactory> > factoryPluginLoader = boost::shared_ptr<FactoryPluginLoader<SensorFactory> >(new FactoryPluginLoader<SensorFactory>(libPaths));
    sensorFactories = factoryPluginLoader->getFactories();
}

MotionReaderXML::MotionReaderXML(const std::map<std::string, boost::shared_ptr<MMM::SensorFactory> > &sensorFactories)
    : sensorFactories(sensorFactories)
{
}

MotionPtr MotionReaderXML::loadMotion(const std::string &xml, const std::string &motionName, bool xmlIsPath) {
    MotionPtr motion;
    RapidXMLReaderPtr reader = xmlIsPath ? RapidXMLReader::FromFile(xml) : RapidXMLReader::FromXmlString(xml);
    RapidXMLReaderNodePtr root = reader->getRoot();
    if (root && root->name() == XML::MOTION_XML_ROOT) {
        if (root->has_attribute(XML::ATTRIBUTE_VERSION) && root->attribute_value(XML::ATTRIBUTE_VERSION) == "2.0") {
            motion = loadMotion(root->first_node_with_attribute(XML::NODE_MOTION, XML::ATTRIBUTE_NAME, motionName), xmlIsPath ? xml : std::string());
        }
        else throw Exception::XMLFormatException(ERROR_MESSAGE + "Please convert old XML with LegacyMotionConverter (e.g. in the MMMViewer: Motion -> Import -> Import legacy MMM motion file) before using this XMLReader!");
    }
    else throw Exception::XMLFormatException(ERROR_MESSAGE + "Node 'MMM' does not exist in " + xml);

    return motion;
}

MotionPtr MotionReaderXML::loadMotion(RapidXMLReaderNodePtr motionNode, const std::string &motionFilePath) {

    std::string motionName = motionNode->attribute_value(XML::ATTRIBUTE_NAME);
    if (motionName.empty()) throw Exception::XMLFormatException(ERROR_MESSAGE + "Motion name should not be empty!");

    ModelPtr originalModel;
    ModelPtr processedModel;
    ModelProcessorPtr modelProcessor;
    if (motionNode->has_node(XML::NODE_MODEL)) {
        RapidXMLReaderNodePtr modelNode = motionNode->first_node(XML::NODE_MODEL);

        ModelReaderXMLPtr mr(new ModelReaderXML());
        std::filesystem::path motionpath(XML::getPath(motionFilePath));
        std::filesystem::path filenamePath(modelNode->attribute_value(XML::ATTRIBUTE_PATH));
        std::filesystem::path result = motionpath;
        result /= filenamePath;
        std::string filename = std::filesystem::absolute(result).string();
        if (!XML::isValidFile(filename))
        {
            // check absolute filename
            filename = modelNode->attribute_value(XML::ATTRIBUTE_PATH);
            if (!XML::isValidFile(filename)) {
                if (handleMissingModelFilePath) handleMissingModelFilePath(filename);
                else throw Exception::XMLFormatException(ERROR_MESSAGE + "Could not determine valid filename: " + filename + ", motion base path:" + motionFilePath);
                if (filename.empty()) throw Exception::XMLFormatException(ERROR_MESSAGE + "No valid motion filename was choosen!");
            }
        }
        originalModel = mr->loadModel(filename);
        processedModel = originalModel->clone();
        if (!originalModel) throw Exception::XMLFormatException(ERROR_MESSAGE + "Could not load model from file " + filename);

        if (modelNode->has_node(XML::NODE_MODELPROCESSOR)) {
            modelProcessor = ModelProcessorFactory::getModelProcessorFromNode(modelNode->first_node(XML::NODE_MODELPROCESSOR)->get_node_ptr());
            processedModel = modelProcessor->convertModel(originalModel);
        }
    }

    MotionPtr motion(new Motion(motionName, originalModel, processedModel, modelProcessor, motionFilePath));

    for (auto sensorNode : motionNode->first_node("Sensors")->nodes(XML::NODE_SENSOR)) {
       std::string sensorVersion = "1.0";
       if (sensorNode->has_attribute(XML::ATTRIBUTE_VERSION)) sensorVersion = sensorNode->attribute_value(XML::ATTRIBUTE_VERSION);
       SensorFactoryPtr factory = sensorFactories[sensorNode->attribute_value(XML::ATTRIBUTE_TYPE) + "_v" + sensorVersion];
       if (factory) motion->addSensor(factory->createSensor(sensorNode)); // Exception will be forwarded
       else MMM_INFO << "Cannot find plugin with id '" << sensorNode->attribute_value(XML::ATTRIBUTE_TYPE) << "' and version '" << sensorVersion << "'" << std::endl;
    }

    return motion;
}

MotionList MotionReaderXML::loadAllMotions(const std::string &xml, bool xmlIsPath) {
    MotionList motions;
    RapidXMLReaderPtr reader = xmlIsPath ? RapidXMLReader::FromFile(xml) : RapidXMLReader::FromXmlString(xml);
    RapidXMLReaderNodePtr root = reader->getRoot();
    std::set<std::string> motionNames;
    if (root && root->name() == XML::MOTION_XML_ROOT) {
        if (root->has_attribute(XML::ATTRIBUTE_VERSION) && root->attribute_value(XML::ATTRIBUTE_VERSION) == "2.0") {
            for (auto motionNode : root->nodes(XML::NODE_MOTION)) {
                MotionPtr motion = loadMotion(motionNode, xmlIsPath ? xml : std::string());
                if (motionNames.find(motion->getName()) != motionNames.end()) throw Exception::XMLFormatException("Reading Motion cancelled, because duplicate of motion name + '" + motion->getName() + "' found. Motion names need to be unique!");
                motionNames.insert(motion->getName());
                motions.push_back(motion);
            }
        }
        else throw Exception::XMLFormatException(ERROR_MESSAGE + "Please convert old XML with LegacyMotionConverter (e.g. in the MMMViewer: Motion -> Import -> Import legacy MMM motion file) before using this XMLReader!");
    }
    else throw Exception::XMLFormatException(ERROR_MESSAGE + "Node 'MMM' does not exist in " + xml);

    return motions;
}

std::string MotionReaderXML::getStandardLibPath() {
    std::string standardLibPath = std::string(SENSOR_PLUGIN_LIB_DIR);
    std::filesystem::path standardLibDir(standardLibPath);
    if (!is_directory(standardLibDir)) return "/usr/lib/MMMSensorPlugins/"; // MMMCore is installed on system
    return standardLibPath;
}
